package org.xx.armory.commons;

import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static java.util.regex.Pattern.CASE_INSENSITIVE;
import static java.util.regex.Pattern.DOTALL;
import static java.util.regex.Pattern.compile;
import static org.xx.armory.commons.Validators.rejectIfNegative;
import static org.xx.armory.commons.Validators.rejectIfNonPositive;
import static org.xx.armory.commons.Validators.rejectIfNull;

/**
 * 文本处理工具类。
 */
public final class TextUtils {
    private static final Pattern STRING_FORMAT_PLACEHOLDER = compile("(?<!\\{)\\{}", DOTALL | CASE_INSENSITIVE);

    private TextUtils() {
        throw new AssertionError();
    }

    public static String[] splitBy(
            String s,
            int separatorChar
    ) {
        if (s == null) {
            return new String[]{"", ""};
        }

        final var p0 = s.indexOf(separatorChar);
        if (p0 < 0) {
            return new String[]{s, ""};
        } else {
            return new String[]{s.substring(0, p0), s.substring(p0 + 1)};
        }
    }

    public static String[] splitByLast(
            String s,
            int separatorChar
    ) {
        if (s == null) {
            return new String[]{"", ""};
        }

        final var p0 = s.lastIndexOf(separatorChar);
        if (p0 < 0) {
            return new String[]{"", s};
        } else {
            return new String[]{s.substring(0, p0), s.substring(p0 + 1)};
        }
    }

    /**
     * 格式化字符串。
     * <p>将占位符{@literal {}}替换为指定的参数。<code>{{</code>被转义为<code>{</code></p>
     *
     * @param f
     *         格式文本。
     * @param values
     *         用于替换的参数。
     * @return 替换的结果，如果参数{@code f}是{@code null}则返回空字符串。
     */
    public static String format(
            String f,
            Object... values
    ) {
        if (f == null) {
            return "";
        }

        // 首先替换所有的占位符，如果给出的参数数量不足则将占位符替换为空字符串。
        return replaceByRegex(STRING_FORMAT_PLACEHOLDER, f, (i, m, last) -> {
            if (values == null) {
                return "";
            } else if (values.length <= i) {
                return "";
            } else {
                final Object value = values[i];
                return value != null ? String.valueOf(value).replace("\\", "\\\\")
                                             .replace("$", "\\$") : "";
            }
        })
                // 然后转义特殊符号。
                .replace("{{", "{")
                .replace("}}", "}");
    }

    /**
     * 使用字符集合进行替换。
     *
     * @param chars
     *         需要被替换的字符集合，如果该参数是空字符串则不执行任何替换。
     * @param src
     *         执行替换的原始字符串，如果此参数是{@code null}则返回空字符串。
     * @param producer
     *         生成替换结果的生成器，每次匹配的时候都会调用此生成器。
     * @return 替换结果。
     * @throws IllegalArgumentException
     *         如果参数 {@code chars} 或者 {@code producer} 是 {@code null}。
     */
    public static String replaceByChars(
            String chars,
            CharSequence src,
            CharReplacementProducer producer
    ) {
        rejectIfNull(chars, "chars");
        rejectIfNull(producer, "producer");

        if (src == null) {
            return "";
        }
        if (chars.isEmpty()) {
            return src.toString();
        }

        final var result = new StringBuilder(src.length());

        int[] temp = new int[]{0};
        src.codePoints().forEach(ch -> {
            if (chars.indexOf(ch) != -1) {
                result.append(producer.execute(temp[0]++, ch));
            } else {
                result.appendCodePoint(ch);
            }
        });

        return result.toString();
    }

    /**
     * 使用正则表达式进行替换。
     * <p>
     * 参数{@code producer}返回的替换字符串中可以包含捕获引用。所以反斜线{@literal \}和美元符{@literal $}具有特殊含义，如果需要把这2个字符看作字面量则需要使用反斜线转义。
     * </p>
     *
     * @param pattern
     *         正则表达式。
     * @param src
     *         执行替换的原始字符串，如果此参数是{@code null}则返回空字符串。
     * @param producer
     *         生成替换结果的生成器，每次匹配的时候都会调用此生成器。
     * @return 替换结果。
     * @throws IllegalArgumentException
     *         如果参数 {@code pattern} 或者 {@code producer} 是 {@code null}。
     * @see Matcher#appendReplacement(StringBuffer, String)
     * @see Matcher#appendTail(StringBuffer)
     * @see ReplacementProducer#execute(int, Matcher, String)
     */
    public static String replaceByRegex(
            Pattern pattern,
            CharSequence src,
            ReplacementProducer producer
    ) {
        rejectIfNull(pattern, "pattern");
        rejectIfNull(producer, "producer");

        if (src == null) {
            return "";
        }

        final StringBuffer buffer = new StringBuffer();
        final Matcher matcher = pattern.matcher(src);
        int i = 0;
        String lastMatched = "";
        while (matcher.find()) {
            matcher.appendReplacement(buffer, producer.execute(i++, matcher, lastMatched));
            lastMatched = matcher.group();
        }
        matcher.appendTail(buffer);

        return buffer.toString();
    }

    /**
     * 计算字符串的缩略形式。
     *
     * <p>如果结果长度足够，自动在结果末尾附加省略号 {@literal ...}。</p>
     *
     * @param src
     *         原始字符串。
     * @param maxLength
     *         最大长度，结果字符串不能超过此长度。
     * @return 字符串的缩略形式。
     */
    public static String ellipse(
            CharSequence src,
            int maxLength
    ) {
        rejectIfNegative(maxLength, "maxLength");

        if (src == null) {
            return "";
        }

        final var l = src.length();
        if (maxLength <= 3) {
            if (l <= maxLength) {
                return src.toString();
            } else {
                return src.subSequence(0, maxLength).toString();
            }
        } else {
            final var ml = maxLength - 3;
            if (l <= ml) {
                return src.toString();
            } else {
                return src.subSequence(0, ml).toString() + "...";
            }
        }
    }

    public static String quote(
            String s
    ) {
        final var result = new StringBuilder();

        result.append('\'');
        if (s != null) {
            s.codePoints().forEach(cp -> {
                if (cp == '\'' || cp == '\\') {
                    result.append('\\');
                }
                result.appendCodePoint(cp);
            });
        }
        result.append('\'');
        return result.toString();
    }

    public static String quoteDouble(
            String s
    ) {
        final var result = new StringBuilder();

        result.append('"');
        if (s != null) {
            s.codePoints().forEach(cp -> {
                if (cp == '"' || cp == '\\') {
                    result.append('\\');
                }
                result.appendCodePoint(cp);
            });
        }
        result.append('"');
        return result.toString();
    }

    /**
     * 判断两个字符串是否相等。
     *
     * @param s1
     *         待比较的字符串。
     * @param s2
     *         另一个待比较的字符串。
     * @return 如果相等则返回{@code true}，否则返回{@code false}，如果两个参数都是{@code null}也返回{@code true}。
     */
    public static boolean stringEquals(
            String s1,
            String s2
    ) {
        return Objects.equals(s1, s2);
    }

    /**
     * 判断两个字符串是否相等，比较时忽略大小写。
     *
     * @param s1
     *         待比较的字符串。
     * @param s2
     *         另一个待比较的字符串。
     * @return 如果相等则返回{@code true}，否则返回{@code false}，如果两个参数都是{@code null}也返回{@code true}。
     */
    public static boolean stringEqualsIgnoreCase(
            String s1,
            String s2
    ) {
        return s1 != null ? s1.equalsIgnoreCase(s2) : s2 == null;
    }

    /**
     * 从字符串中删除所有的空白字符。
     *
     * <p>
     * 此方法通常用于过滤所有的空白字符。<strong>和 {@link String#trim()} 不同，任何位置上的空白字符都会被删除。</strong>
     * </p>
     *
     * @param s
     *         原字符串
     * @return 删除了所有空白字符之后的字符串，如果{@code s == null}则返回空字符串。
     * @see Character#isWhitespace(int)
     */
    public static String removeWhitespace(
            String s
    ) {
        return s != null ? s.codePoints()
                            .filter(cp -> !Character.isWhitespace(cp))
                            .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
                            .toString() : "";
    }

    /**
     * 从字符串中删除所有的非数字字符。
     *
     * <p>
     * 此方法通常用于过滤所有的非数字字符，使得字符串能够通过{@code Integer.valueOf()}之类的方法。
     * </p>
     *
     * @param s
     *         原字符串
     * @return 删除了所有非数字字符之后的字符串，如果{@code s == null}则返回空字符串。
     * @see Character#isDigit(int)
     */
    public static String removeNonDigit(
            String s
    ) {
        return s != null ? s.codePoints()
                            .filter(cp -> Character.isDigit(cp) || cp == '+' || cp == '-' || cp == '.')
                            .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
                            .toString() : "";
    }

    /**
     * 从字符串中删除所有的非十六进制数字字符。
     *
     * <p>
     * 此方法通常用于过滤所有的非十六进制数字字符，使得字符串能够通过{@code Integer.valueOf()}之类的方法。
     * </p>
     *
     * @param s
     *         原字符串
     * @return 删除了所有非数字字符之后的字符串，如果{@code s == null}则返回空字符串。
     */
    public static String removeNonHex(
            String s
    ) {
        return s != null ? s.codePoints()
                            .filter(cp -> Character.isDigit(cp) || (cp >= 'a' && cp <= 'f') || (cp >= 'A' && cp <= 'F'))
                            .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append)
                            .toString() : "";
    }

    /**
     * 将代码点转化为UTF8序列。
     *
     * @param ch
     *         待转化的代码点。
     * @return 转化结果。
     * @throws UnmappableCodePointException
     *         如果指定的代码点无法被转化。
     */
    public static byte[] codePointToUtf8(
            int ch
    ) {
        if (ch <= 0x80) {
            // 0xxxxxxx
            return new byte[]{(byte) (ch & 0x7f)};
        } else if (ch <= 0x800) {
            // 110xxxxx 10xxxxxx
            return new byte[]{(byte) (((ch & 0x7c0) >> 6) | 0xc0), (byte) ((ch & 0x3f) | 0x80)};
        } else if (ch <= 0x10000) {
            // 1110xxxx 10xxxxxx 10xxxxxx
            return new byte[]{(byte) (((ch & 0xf000) >> 12) | 0xe0), (byte) (((ch & 0xfc0) >> 6) | 0x80), (byte) ((ch & 0x3f) | 0x80)};
        } else if (ch <= 0x110000) {
            // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
            return new byte[]{(byte) (((ch & 0x1c0000) >> 18) | 0xc0), (byte) (((ch & 0x3f000) >> 12) | 0x80), (byte) (((ch & 0xfc0) >> 6) | 0x80), (byte) ((ch & 0x3f) | 0x80)};
        } else {
            throw new UnmappableCodePointException(ch);
        }
    }

    /**
     * 将整数转化为数字，并在左侧加入前导0。
     *
     * @param i
     *         待转化的数字。
     * @param l
     *         加入前导0后的总长度。
     * @return 加入前导0之后的字符串。
     */
    public static String leftPadZero(
            int i,
            int l
    ) {
        rejectIfNonPositive(l, "l");

        final var s = String.valueOf(i);
        final var sl = s.length();
        if (sl < l) {
            return "0".repeat(l - sl) + s;
        } else {
            return s;
        }
    }

    /**
     * 生成缩略文本。
     *
     * @param src
     *         原始文本。
     * @param maxLength
     *         最大长度。
     * @return 缩略文本。
     * @throws IllegalArgumentException
     *         如果参数{@code maxLength}小于或者等于0。
     */
    public static String abbreviationText(
            String src,
            int maxLength
    ) {
        rejectIfNonPositive(maxLength, "maxLength");

        if (src == null) {
            return "";
        }

        if (src.length() <= maxLength) {
            return src;
        }

        return src.substring(0, maxLength <= 3 ? maxLength : maxLength - 3) + "...";
    }

    @FunctionalInterface
    public interface CharReplacementProducer {
        String execute(
                int index,
                int ch
        );
    }

    /**
     * 用于生成标记替换结果的生成器。
     */
    @FunctionalInterface
    public interface ReplacementProducer {
        /**
         * 为标记生成替换结果。
         *
         * @param index
         *         替换序号，从0开始。
         * @param matcher
         *         用于匹配标记的匹配器。
         * @param lastMatched
         *         最近一次匹配的结果，如果当前是第一次匹配则此参数的值是空字符串。
         * @return 替换结果。
         */
        String execute(
                int index,
                Matcher matcher,
                String lastMatched
        );
    }
}
